package ru.batrdmi.svnplugin;

import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.ide.DataManager;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.progress.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.FrameWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vcs.history.CurrentRevision;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.history.VcsHistoryUtil;
import com.intellij.openapi.vcs.ui.ReplaceFileConfirmationDialog;
import com.intellij.openapi.vcs.vfs.ContentRevisionVirtualFile;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.components.JBScrollPane;
import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.shape.mxRectangleShape;
import com.mxgraph.swing.handler.mxSelectionCellsHandler;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.*;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxStylesheet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnRevisionNumber;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.actions.ShowAllSubmittedFilesAction;
import org.jetbrains.idea.svn.history.SvnFileRevision;
import org.jetbrains.idea.svn.history.SvnRepositoryContentRevision;
import org.tmatesoft.svn.core.wc.SVNRevision;
import ru.batrdmi.svnplugin.gui.ScrollOverlayFactory;
import ru.batrdmi.svnplugin.logic.FileHistoryRetriever;
import ru.batrdmi.svnplugin.logic.FileNameUtil;
import ru.batrdmi.svnplugin.logic.FileRevisionHistory;
import ru.batrdmi.svnplugin.logic.Revision;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;

public class SVNRevisionGraph extends FrameWrapper implements mxEventSource.mxIEventListener {
    private static final Logger log = Logger.getInstance("ru.batrdmi.svnplugin.SVNRevisionGraph");
    private static final int CELL_SPACING = 15;
    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd  HH:mm:ss";
    private static final String IN_BRANCH_LINK_STYLE = "inBranchLinkStyle";
    private static final String COPY_OR_MERGE_LINK_STYLE = "copyOrMergeLinkStyle";
    private static final String HIGHLIGHTED_COPY_OR_MERGE_LINK_STYLE = "highlightedCopyOrMergeLinkStyle";
    private static final String REVISION_STYLE = "revisionStyle";
    private static final String REVISION_SHAPE = "revisionShape";

    private static final Color REVISION_COLOR = new Color(185, 230, 184);
    private static final Color SELECTED_REVISION_COLOR = new Color(53, 128, 51);
    private static final Color BRANCH_NAMES_COLOR = Color.GRAY;
    private static final int TEXTFIELD_WIDTH_IN_CHARS = 16;

    private static final SimpleDateFormat _dateTimeFormat = new SimpleDateFormat(DATE_TIME_FORMAT);

    static {
        mxGraphics2DCanvas.putShape(REVISION_SHAPE, new RevisionShape());
    }

    public static void createAndShow(Project project, FilePath filePath) {
        boolean extendedScan = SVNRevisionGraphConfiguration.getInstance(project).isExtendedScanEnabled();
        createOrUpdateGraph(null, project, filePath, extendedScan);
    }


    private static void createOrUpdateGraph(@Nullable final SVNRevisionGraph graph, final Project project,
                                                        final FilePath filePath, final boolean extendedScan) {
        if (graph != null) {
            graph.refreshInProgress = true;
        }
        new Task.Backgroundable(project, "Revision History", true, PerformInBackgroundOption.DEAF){
            private SVNRevisionGraph myGraph;
            private Exception myException;

            @Override
            public void run(@NotNull ProgressIndicator indicator) {
                indicator.setText("Collecting revision history for file: " + filePath.getName());
                try {
                    SvnVcs svn = SvnVcs.getInstance(project);
                    FileRevisionHistory frh = new FileHistoryRetriever(svn).getFileHistory(filePath.getVirtualFile(),
                            extendedScan, indicator);
                    indicator.setText("Creating graph");
                    indicator.setText2(null);
                    if (graph == null) {
                        myGraph = new SVNRevisionGraph(project, filePath, frh, extendedScan);
                    } else {
                        myGraph = graph;
                        myGraph.revisionHistory = frh;
                    }
                    myGraph.initGraphData();
                } catch (Exception e) {
                    myException = e;
                }
            }

            @Override
            public void onSuccess() {
                if (myException == null) {
                    if (graph == null) {
                        myGraph.initComponents();
                        myGraph.show();
                    }
                    myGraph.setCurrentRevision();
                    if (myGraph.revisionHistory.isRetrievedWithErrors()) {
                        Notifications.Bus.notify(new Notification("SvnRevisionGraph",
                                "Error collecting revision information",
                                "Displayed graph may be incorrect or incomplete. More details are in IDEA's log",
                                NotificationType.ERROR), project);
                    }
                } else {
                    log.error("Error retrieving file history", myException);
                    Messages.showErrorDialog(project, "Error retrieving file history", "Revision Graph Error");
                }
                if (graph != null) {
                    graph.refreshInProgress = false;
                }
            }

            @Override
            public void onCancel() {
                if (graph != null) {
                    graph.refreshInProgress = false;
                }
            }
        }.queue();
    }

    private FilePath _filePath = null;
    private Project _project = null;
    private FileRevisionHistory revisionHistory;
    private mxGraph _g = null;
    private mxGraphComponent gc = null;
    private List<Revision> _revisions = null;
    private JTextField _revisionTF = null;
    private JTextField _authorTF = null;
    private JTextField _dateTF = null;
    private JTextArea _messageTA = null;
    private LinkedHashMap<String, ArrayList<Revision>> _revMap = null;
    private LinkedHashMap<String, ArrayList<Revision>> _filteredRevMap = null;
    private HashMap<Revision, mxCell> _cellMap;
    private Revision currentRevision;
    private ActionPopupMenu viewRevisionPopup;
    private boolean extendedScan;
    private boolean refreshInProgress;

    public SVNRevisionGraph(Project project, FilePath filePath, FileRevisionHistory fileRevisionHistory,
                            boolean extendedScan) {
        super(project, "SvnRevisionGraphPlugin.dimensions");
        _filePath = filePath;
        _project = project;
        revisionHistory = fileRevisionHistory;
        this.extendedScan = extendedScan;
        setTitle("Revision Graph: " + filePath.getPath());
        try {
            setImage(ImageIO.read(getClass().getResource("/icons/svngraph.png")));
        } catch (IOException e) {
            log.error("Error loading icon", e);
        }
        setProject(project);
        _g = new mxGraph() {
            @Override
            public String getLabel(Object cell) {
                Revision rev = (Revision) ((mxCell) cell).getValue();
                return (rev == null) ? null : Long.toString(rev.getRevisionNumber());
            }

            @Override
            public String getToolTipForCell(Object cell) {
                Revision rev = (Revision) ((mxCell) cell).getValue();
                return (rev == null) ? null : rev.getMessage();
            }

            @Override
            protected mxStylesheet createStylesheet() {
                mxStylesheet s = new mxStylesheet();

                Map<String, Object> style = new HashMap<String, Object>();
                style.put(mxConstants.STYLE_SHAPE, mxConstants.SHAPE_CONNECTOR);
                style.put(mxConstants.STYLE_STROKECOLOR, "black");
                style.put(mxConstants.STYLE_ENDARROW, "no arrow");
                s.putCellStyle(IN_BRANCH_LINK_STYLE, new HashMap<String, Object>(style));

                style.put(mxConstants.STYLE_ENDARROW, mxConstants.ARROW_CLASSIC);
                s.putCellStyle(COPY_OR_MERGE_LINK_STYLE, new HashMap<String, Object>(style));

                style.put(mxConstants.STYLE_STROKECOLOR, "blue");
                s.putCellStyle(HIGHLIGHTED_COPY_OR_MERGE_LINK_STYLE, new HashMap<String, Object>(style));

                style.clear();
                style.put(mxConstants.STYLE_SHAPE, REVISION_SHAPE);
                style.put(mxConstants.STYLE_STROKECOLOR, "black");
                style.put(mxConstants.STYLE_FONTCOLOR, "black");
                style.put(mxConstants.STYLE_SPACING, 1);
                style.put(mxConstants.STYLE_SPACING_TOP, 2);
                style.put(mxConstants.STYLE_SPACING_LEFT, 1);
                s.putCellStyle(REVISION_STYLE, new HashMap<String, Object>(style));

                return s;
            }
        };
        _g.setCellsMovable(false);
        _g.setCellsEditable(false);
        _g.setCellsResizable(false);
        _g.setDropEnabled(false);
    }

    public void initGraphData() {
        getAllRevisions();
        _g.setModel(new mxGraphModel());
        _g.getModel().beginUpdate();
        try {
            createGraph();
            layoutGraph();
        } finally {
            _g.getModel().endUpdate();
        }
    }

    // should be called in EDT
    public void initComponents() {
        gc = new mxGraphComponent(_g) {
            @Override
            protected mxSelectionCellsHandler createSelectionCellsHandler() {
                return new mxSelectionCellsHandler(this) {
                    @Override
                    public void paintHandles(Graphics g) {
                    }
                };
            }
        };
        gc.getConnectionHandler().setEnabled(false);
        gc.getGraphHandler().setMoveEnabled(false);
        gc.getGraphHandler().setMarkerEnabled(false);
        gc.getGraphHandler().setVisible(false);
        gc.getSelectionCellsHandler().setMaxHandlers(0);
        gc.setDragEnabled(false);
        createComponents();
        addMouseHandlers();
        createPopupActions();
    }

    private void getAllRevisions() {
        _revMap = new LinkedHashMap<String, ArrayList<Revision>>();
        currentRevision = revisionHistory.getCurrentRevision();
        _revisions = new ArrayList<Revision>(revisionHistory.getAllRevisions());
        // Sort revisions based on revision dates
        Collections.sort(_revisions, new Revision.RevisionNumberComparator());
        // Fill revMap
        for (Revision rev : _revisions) {
            String path = rev.getRelPath();
            ArrayList<Revision> revsOnPath = _revMap.get(path);
            if (revsOnPath == null) {
                revsOnPath = new ArrayList<Revision>();
                _revMap.put(path, revsOnPath);
            }
            revsOnPath.add(rev);
        }
    }

    private void createGraph() {
        _filteredRevMap = new LinkedHashMap<String, ArrayList<Revision>>(_revMap);
        _cellMap = new HashMap<Revision, mxCell>();
        for (String path : _filteredRevMap.keySet()) {
            mxCell lastCell = null;
            ArrayList<Revision> revsOnPath = _filteredRevMap.get(path);
            for (Revision rev : revsOnPath) {
                lastCell = createCell(lastCell, rev);
            }
        }
        // create merge/copy links
        for (mxCell cell : _cellMap.values()) {
            Revision rev = (Revision) cell.getValue();
            mxCell mergeFromCell = getLinkedCell(rev);
            if (mergeFromCell != null) {
                createEdge(mergeFromCell, cell, true);
            }
        }
        // move all vertices to foreground
        _g.orderCells(false, _cellMap.values().toArray());
    }

    private mxCell createCell(mxCell neighborCell, Revision rev) {
        mxCell retVal = new mxCell(rev, new mxGeometry(), null);
        retVal.setVertex(true);
        retVal.setConnectable(true);
        retVal.setStyle(REVISION_STYLE + ";"
                + mxConstants.STYLE_STROKEWIDTH + "=" + ((rev == currentRevision) ? 3 : 1) + ";"
                + mxConstants.STYLE_DASHED + "=" + rev.isDeleted());
        _cellMap.put(rev, retVal);
        _g.addCell(retVal);
        if (neighborCell != null && !((Revision) neighborCell.getValue()).isDeleted()) {
            createEdge(neighborCell, retVal, false);
        }
        return retVal;
    }

    private mxCell createEdge(mxCell source, mxCell target, boolean arrow) {
        return (mxCell) _g.insertEdge(null, null, null, source, target,
                arrow ? COPY_OR_MERGE_LINK_STYLE : IN_BRANCH_LINK_STYLE);
    }

    private mxCell getLinkedCell(Revision rev) {
        if (rev.getLinkedRelPath() == null) {
            return null;
        } else {
            Revision mergeRev = new Revision(rev.getLinkedRelPath(), rev.getLinkedRevisionNumber());
            return _cellMap.get(mergeRev);
        }
    }

    private void layoutGraph() {
        mxCell lastCell = null;
        Double nextY = null;
        int i = 0;
        for (String path : _filteredRevMap.keySet()) {
            ArrayList<Revision> revsOnPath = _filteredRevMap.get(path);
            if (i != 0) {
                Revision firstRev = revsOnPath.get(0);
                lastCell = getLinkedCell(firstRev);
            }
            int relativePosition = SwingConstants.BOTTOM;
            Double widestY = null;
            for (Revision rev : revsOnPath) {
                mxCell cell = _cellMap.get(rev);
                lastCell = positionCell(lastCell, relativePosition, nextY, cell);
                relativePosition = SwingConstants.RIGHT;
                mxRectangle rect = lastCell.getGeometry();
                if (widestY == null)
                    widestY = rect.getY() + rect.getHeight();
                else {
                    double wideY = rect.getY() + rect.getHeight();
                    if (widestY < wideY)
                        widestY = wideY;
                }
            }
            nextY = widestY + CELL_SPACING;
            i++;
        }
        //Now adjust x position in order of revision numbers
        for (i = 1; i < _revisions.size(); i++) {
            Revision rev = _revisions.get(i);
            Revision prevRev = _revisions.get(i - 1);
            mxCell cell = _cellMap.get(rev);
            mxCell prevCell = _cellMap.get(prevRev);
            Rectangle rect = cell.getGeometry().getRectangle();
            positionCell(prevCell,
                    rev.getRevisionNumber() > prevRev.getRevisionNumber() ? SwingConstants.RIGHT : SwingConstants.BOTTOM,
                    rect.getY(), cell);
        }
    }

    private mxCell positionCell(mxCell neighborCell, int relativePosition, Double preferredY, mxCell targetCell) {
        //Calculate position from neighbor
        mxRectangle neighborRect = null;
        if (neighborCell == null) {
            if (relativePosition == SwingConstants.BOTTOM)
                neighborRect = new mxRectangle(CELL_SPACING, 0, 0, 0);
            else if (relativePosition == SwingConstants.RIGHT)
                neighborRect = new mxRectangle(0, CELL_SPACING, 0, 0);
        } else {
            neighborRect = neighborCell.getGeometry();
        }
        double x = 0, y = 0;
        if (relativePosition == SwingConstants.BOTTOM) {
            x = neighborRect.getX();
            y = neighborRect.getY() + neighborRect.getHeight() + CELL_SPACING;
        } else if (relativePosition == SwingConstants.RIGHT) {
            x = neighborRect.getX() + neighborRect.getWidth() + CELL_SPACING;
            y = neighborRect.getY();
        }
        if (preferredY != null) {
            y = preferredY;
        }

        mxRectangle preferredSize = _g.getPreferredSizeForCell(targetCell);
        neighborRect = new mxRectangle(x, y, preferredSize.getWidth(), preferredSize.getHeight());
        _g.resizeCell(targetCell, neighborRect);
        return targetCell;
    }

    private void createComponents() {
        JPanel p = new JPanel(new BorderLayout());
        p.add(createNorthPanel(), BorderLayout.NORTH);
        p.add(createCenterPanel(), BorderLayout.CENTER);
        p.add(createSouthPanel(), BorderLayout.SOUTH);
        setComponent(p);
    }

    private JComponent createNorthPanel() {
        DefaultActionGroup toolBarActions = new DefaultActionGroup(
                new CompareAction(),
                new MyGetVersionAction(),
                new ShowAllSubmittedFilesAction(),
                new RefreshAction());
        toolBarActions.addAction(new EnableExtendedScanAction()).setAsSecondary(true);
        ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR,
                toolBarActions, true);
        return actionToolbar.getComponent();
    }

    private void createPopupActions() {
        ActionManager am = ActionManager.getInstance();

        DefaultActionGroup popupActions = new DefaultActionGroup(
                am.getAction(IdeActions.ACTION_EDIT_SOURCE),
                new CompareAction(),
                new MyGetVersionAction(),
                new ShowAllSubmittedFilesAction());
        viewRevisionPopup = am.createActionPopupMenu(ActionPlaces.CHANGES_VIEW_POPUP, popupActions);
    }

    @Nullable
    protected JComponent createCenterPanel() {
        _g.getSelectionModel().addListener(mxEvent.CHANGE, this);
        JBScrollPane scrollPane = new JBScrollPane(gc.getGraphControl());
        scrollPane.getViewport().setBackground(Color.WHITE);
        scrollPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, this);
        return ScrollOverlayFactory.addOverlay(scrollPane, new BranchNamesOverlay(), false, true);
    }

    @Nullable
    protected JComponent createSouthPanel() {
        GridBagLayout gbl = new GridBagLayout();
        JPanel retVal = new JPanel(gbl);
        retVal.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.EAST,
                GridBagConstraints.NONE, new Insets(5, 5, 0, 0), 0, 0);
        retVal.add(new JLabel("Revision:"), gbc);
        gbc.gridy = 1;
        retVal.add(new JLabel("Author:"), gbc);
        gbc.gridy = 2;
        retVal.add(new JLabel("Date:"), gbc);
        gbc.gridx = 1;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.CENTER;
        _revisionTF = new JTextField(TEXTFIELD_WIDTH_IN_CHARS);
        _revisionTF.setMinimumSize(_revisionTF.getPreferredSize());
        _revisionTF.setEditable(false);
        _revisionTF.setBackground(Color.WHITE);
        retVal.add(_revisionTF, gbc);
        gbc.gridy = 1;
        _authorTF = new JTextField(TEXTFIELD_WIDTH_IN_CHARS);
        _authorTF.setMinimumSize(_authorTF.getPreferredSize());
        _authorTF.setEditable(false);
        _authorTF.setBackground(Color.WHITE);
        retVal.add(_authorTF, gbc);
        gbc.gridy = 2;
        _dateTF = new JTextField(TEXTFIELD_WIDTH_IN_CHARS);
        _dateTF.setMinimumSize(_dateTF.getPreferredSize());
        _dateTF.setEditable(false);
        _dateTF.setBackground(Color.WHITE);
        retVal.add(_dateTF, gbc);
        gbc.gridx = 2;
        gbc.gridy = 0;
        gbc.gridheight = 3;
        gbc.anchor = GridBagConstraints.NORTHEAST;
        retVal.add(new JLabel("Message:"), gbc);
        gbc.gridx = 3;
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 1;
        _messageTA = new JTextArea();
        _messageTA.setEditable(false);
        JScrollPane sp = new JBScrollPane(_messageTA);
        sp.setPreferredSize(new Dimension(300, 0));
        retVal.add(sp, gbc);
        return retVal;
    }

    private void setCurrentRevision() {
        mxCell cell = _cellMap.get(currentRevision);
        _g.setSelectionCell(cell);
        gc.scrollCellToVisible(cell);
    }

    private void addMouseHandlers() {
        gc.getGraphControl().addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                maybeShowPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (gc.getGraphControl().isEnabled() && !e.isConsumed() && e.getClickCount() == 2) {
                    mxCell cell = (mxCell) gc.getCellAt(e.getX(), e.getY(), false);
                    if (!revisionHistory.isDirectory() && cell != null && cell.isEdge()) {
                        mxCell cell1 = (mxCell) cell.getSource();
                        mxCell cell2 = (mxCell) cell.getTarget();
                        Revision rev1 = (Revision) cell1.getValue();
                        Revision rev2 = (Revision) cell2.getValue();
                        compareFileRevisions(rev1, rev2);
                    }
                }
                maybeShowPopup(e);
            }

            private void maybeShowPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    mxCell cell = (mxCell) gc.getCellAt(e.getX(), e.getY());
                    if (cell != null && cell.isVertex()) {
                        viewRevisionPopup.getComponent().show(e.getComponent(), e.getX(), e.getY());
                    }
                }
            }
        });
        gc.getGraphControl().addMouseMotionListener(new MouseAdapter() {
            private mxCell highlightedEdge;


            @Override
            public void mouseMoved(MouseEvent e) {
                mxCell cell = (mxCell) gc.getCellAt(e.getX(), e.getY());
                if (cell != highlightedEdge) {
                    if (highlightedEdge != null) {
                        highlightCell(highlightedEdge, false);
                    }
                    if (cell != null && isHighlightable(cell)) {
                        highlightCell(cell, true);
                        highlightedEdge = cell;
                    } else {
                        highlightedEdge = null;
                    }
                }
            }
       });
    }

    private void highlightCell(mxCell cell, boolean on) {
        _g.setCellStyle(on ? HIGHLIGHTED_COPY_OR_MERGE_LINK_STYLE : COPY_OR_MERGE_LINK_STYLE,
                new Object[]{cell});
    }

    private boolean isHighlightable(mxCell cell) {
        return cell.isEdge() && !cell.getStyle().equals(IN_BRANCH_LINK_STYLE);
    }

    @Override
    public void dispose() {
        _revMap = null;
        _filteredRevMap = null;
        _cellMap = null;
        _g = null;
        gc = null;
        _project = null;
        _filePath = null;
        revisionHistory = null;
        _revisions = null;
        currentRevision = null;
        super.dispose();
    }

    private void compareFileRevisions(Revision rev1, Revision rev2) {
        if (new Revision.RevisionNumberComparator().compare(rev1, rev2) > 0) {
            Revision tempRev = rev2;
            rev2 = rev1;
            rev1 = tempRev;
        }
        try {
            VcsFileRevision r1 = createVcsFileRevision(rev1, true);
            VcsFileRevision r2 = createVcsFileRevision(rev2, true);
            VcsHistoryUtil.showDiff(_project, _filePath, r1, r2,
                    rev1.getRelPath() + "@" + rev1.getRevisionNumber() + ((currentRevision == rev1) ? "(Local)" : ""),
                    rev2.getRelPath() + "@" + rev2.getRevisionNumber() + ((currentRevision == rev2) ? "(Local)" : ""));
        } catch (Exception e) {
            log.error("Error displaying diff", e);
            Messages.showErrorDialog(_project, "Error showing diff", "Error");
        }
    }

    private String getPathDisplayName(String path, String sourcePath) {
        String[] pathSplit = FileNameUtil.splitPath(path);
        String[] sourceSplit = FileNameUtil.splitPath(sourcePath);
        if (pathSplit != null && sourceSplit != null
                && pathSplit[0].equals(sourceSplit[0])
                && pathSplit[2].equals(sourceSplit[2])) {
            String innerPart = pathSplit[1];
            if (innerPart.equals("/trunk")) {
                return "[trunk]";
            } else if (innerPart.startsWith("/branches/")) {
                return '[' + innerPart.substring(10) + ']';
            } else if (innerPart.startsWith("/tags/")) {
                return "[Tag: " + innerPart.substring(6) + ']';
            }
        }
        return path;
    }

    @Override
    public void invoke(Object sender, mxEventObject evt) {
        selectionChanged();
    }

    public void selectionChanged() {
        Object selCells[] = _g.getSelectionCells();
        Object selectedRev1 = null;
        Object selectedRev2 = null;
        if ((selCells != null) && (selCells.length == 1)) {
            mxCell selCell = (mxCell) selCells[0];
            if (selCell.isEdge()) {
                mxCell cell1 = (mxCell) selCell.getSource();
                Object user1 = cell1.getValue();
                mxCell cell2 = (mxCell) selCell.getTarget();
                Object user2 = cell2.getValue();
                if ((user1 instanceof Revision) && (user2 instanceof Revision)) {
                    selectedRev1 = user1;
                    selectedRev2 = user2;
                }
            } else if (selCell.isVertex()) {
                selectedRev1 = selCell.getValue();
            }
        }
        if ((selectedRev1 == null) || (selectedRev2 != null)) {
            _revisionTF.setText(null);
            _authorTF.setText(null);
            _dateTF.setText(null);
            _messageTA.setText(null);
        } else {
            Revision rev = (Revision) selectedRev1;
            _revisionTF.setText("" + rev.getRevisionNumber());
            _authorTF.setText(rev.getAuthor());
            Date d = rev.getDate();
            if (d != null)
                _dateTF.setText(_dateTimeFormat.format(d));
            _messageTA.setText(rev.getMessage());
            _messageTA.setCaretPosition(0); // to prevent scrolling to the end of message
        }
    }

    private List<Revision> getSelectedRevisions() {
        List<Revision> revs = new ArrayList<Revision>();
        if (_g != null) {
            Object[] cells = _g.getSelectionCells();
            if (cells != null) {
                for (Object cell : cells) {
                    mxCell c = (mxCell) cell;
                    if (c.isVertex()) {
                        revs.add((Revision) c.getValue());
                    }
                }
            }
        }
        return revs;
    }

    @Override
    public Object getData(@NonNls String dataId) {
        List<Revision> selectedRevisions = getSelectedRevisions();
        if (PlatformDataKeys.NAVIGATABLE.is(dataId)) {
            if (selectedRevisions.size() == 1 && !selectedRevisions.get(0).isDeleted()) {
                VirtualFile vf = createVirtualFileForSelectedRevision();
                return new OpenFileDescriptor(_project, vf);
            }
        } else if (PlatformDataKeys.PROJECT.is(dataId)) {
            return _project;
        } else if (VcsDataKeys.VCS_VIRTUAL_FILE.is(dataId)) {
            if (selectedRevisions.size() == 1) {
                return createVirtualFileForSelectedRevision();
            }
        } else if (VcsDataKeys.VCS_FILE_REVISION.is(dataId)) {
            if (selectedRevisions.size() == 1) {
                SvnVcs svnVcs = SvnVcs.getInstance(_project);
                SVNRevision svnRev = SVNRevision.create(selectedRevisions.get(0).getRevisionNumber());
                return new SvnFileRevision(svnVcs, svnRev, svnRev,
                        revisionHistory.getRepoRoot(), null, null, null, null);
            }
        }
        return null;
    }

    private VirtualFile createVirtualFileForSelectedRevision() {
        Revision r = getSelectedRevisions().get(0);
        ContentRevision cr = SvnRepositoryContentRevision.create(SvnVcs.getInstance(_project),
                revisionHistory.getRepoRoot(), r.getRelPath(), null, r.getRevisionNumber());
        return ContentRevisionVirtualFile.create(cr);
    }

    private VcsFileRevision createVcsFileRevision(Revision r, boolean checkCurrentRevision) {
        SVNRevision svnRev = SVNRevision.create(r.getRevisionNumber());
        if (checkCurrentRevision && r == currentRevision) {
            return new CurrentRevision(_filePath.getVirtualFile(), new SvnRevisionNumber(svnRev));
        } else {
            SvnVcs svnVcs = SvnVcs.getInstance(_project);
            return new SvnFileRevision(svnVcs, svnRev, svnRev,
                    revisionHistory.getRepoRoot() + r.getRelPath(), null, null, null, null);
        }
    }

    private void refresh() {
        createOrUpdateGraph(this, _project, _filePath, extendedScan);
    }

    private class BranchNamesOverlay extends JComponent {
        @Override
        public void paint(Graphics g) {
            if (_cellMap != null && !_cellMap.isEmpty()) {
                mxCell cell = _cellMap.values().iterator().next();
                double step = cell.getGeometry().getHeight() + CELL_SPACING;
                int currentY = CELL_SPACING - 3;
                g.setColor(BRANCH_NAMES_COLOR);
                FontMetrics fm = g.getFontMetrics();
                for (String path : _filteredRevMap.keySet()) {
                    String displayedPath = getPathDisplayName(path, revisionHistory.getRelPath());
                    g.drawString(" " + displayedPath, 0, currentY);
                    int lineWidth = fm.stringWidth(" " + displayedPath + " ");
                    g.drawLine(0, currentY + 1, lineWidth, currentY + 1);
                    currentY += step;
                }
            }
        }
    }

    protected class CompareAction extends AnAction {
        public CompareAction() {
            super("Compare with local", "Compare with local version", IconLoader.getIcon("/actions/diff.png"));
        }

        public void actionPerformed(AnActionEvent ae) {
            List<Revision> revisions = getSelectedRevisions();
            int selectionSize = revisions.size();
            if (selectionSize == 2) {
                compareFileRevisions(revisions.get(0), revisions.get(1));
            } else if (selectionSize == 1) {
                compareFileRevisions(revisions.get(0), currentRevision);
            }
        }

        @Override
        public void update(AnActionEvent e) {
            List<Revision> selection = getSelectedRevisions();
            int selectionSize = selection.size();
            Presentation p = e.getPresentation();
            if (selectionSize == 1) {
                p.setText("Compare with local");
                p.setDescription("Compare with local version");
            } else {
                p.setText("Compare");
                p.setDescription("Compare revisions");
            }
            boolean enabled = !revisionHistory.isDirectory()
                    && ((selectionSize == 1 && !selection.get(0).isDeleted())
                        || (selectionSize == 2 && !selection.get(0).isDeleted() && !selection.get(1).isDeleted()));
            p.setEnabled(enabled);
            if (ActionPlaces.isPopupPlace(e.getPlace())) {
                p.setVisible(enabled);
            }
        }
    }

    // Class 'mostly' copied from com.intellij.openapi.vcs.history.FileHistoryPanelImpl
    private class MyGetVersionAction extends AnAction {
        public MyGetVersionAction() {
            super("Get", "Get version from repository", IconLoader.getIcon("/actions/get.png"));
        }

        public void update(AnActionEvent e) {
            Presentation presentation = e.getPresentation();
            if (_filePath.getVirtualFileParent() == null) {
                presentation.setVisible(false);
                presentation.setEnabled(false);
            } else {
                boolean enabled = isEnabled();
                presentation.setEnabled(enabled);
                presentation.setVisible(enabled || !ActionPlaces.isPopupPlace(e.getPlace()));
            }
        }

        public boolean isEnabled() {
            List<Revision> selection = getSelectedRevisions();
            return !revisionHistory.isDirectory() && selection.size() == 1 && !selection.get(0).isDeleted();
        }

        @Override
        public void actionPerformed(AnActionEvent ae) {
            if (!isEnabled()) {
                return;
            }
            final VcsFileRevision revision = createVcsFileRevision(getSelectedRevisions().get(0), false);
            if (_filePath.getVirtualFile() != null) {
                if (!new ReplaceFileConfirmationDialog(_project, "Get revision")
                        .confirmFor(new VirtualFile[]{_filePath.getVirtualFile()})) {
                    return;
                }
            }

            try {
                revision.loadContent();
            } catch (VcsException e) {
                Messages.showErrorDialog("Cannot load version: " + e.getLocalizedMessage(), "Get version");
            } catch (ProcessCanceledException ex) {
                return;
            }
            getVersion(revision);
            refreshFile(revision);
        }

        private void refreshFile(VcsFileRevision revision) {
            Runnable refresh = null;
            final VirtualFile vf = _filePath.getVirtualFile();
            if (vf == null) {
                final LocalHistoryAction action = startLocalHistoryAction(revision);
                final VirtualFile vp = _filePath.getVirtualFileParent();
                if (vp != null) {
                    refresh = new Runnable() {
                        public void run() {
                            vp.refresh(false, true, new Runnable() {
                                public void run() {
                                    _filePath.refresh();
                                    action.finish();
                                }
                            });
                        }
                    };
                }
            } else {
                refresh = new Runnable() {
                    public void run() {
                        vf.refresh(false, false);
                    }
                };
            }
            if (refresh != null) {
                ProgressManager.getInstance().runProcessWithProgressSynchronously(refresh, "Refreshing files...", false, _project);
            }
        }

        private void getVersion(final VcsFileRevision revision) {
            final VirtualFile file = _filePath.getVirtualFile();
            if ((file != null) && !file.isWritable()) {
                if (ReadonlyStatusHandler.getInstance(_project).ensureFilesWritable(file).hasReadonlyFiles()) {
                    return;
                }
            }

            LocalHistoryAction action = file != null ? startLocalHistoryAction(revision) : LocalHistoryAction.NULL;

            final byte[] revisionContent;
            try {
                revision.loadContent();
                revisionContent = revision.getContent();
            } catch (IOException e) {
                log.error(e);
                return;
            } catch (VcsException e) {
                Messages.showMessageDialog("Cannot load revision: " + e.getLocalizedMessage(),
                        "Get Revision Content", Messages.getInformationIcon());
                return;
            } catch (ProcessCanceledException ex) {
                return;
            }
            try {
                ApplicationManager.getApplication().runWriteAction(new Runnable() {
                    public void run() {
                        CommandProcessor.getInstance().executeCommand(_project, new Runnable() {
                            public void run() {
                                try {
                                    write(revisionContent);
                                } catch (IOException e) {
                                    Messages.showMessageDialog("Cannot save content: " + e.getLocalizedMessage(),
                                            "Get Revision Content", Messages.getErrorIcon());
                                }
                            }
                        }, createGetActionTitle(revision), null);
                    }
                });
                if (file != null) {
                    VcsDirtyScopeManager.getInstance(_project).fileDirty(file);
                }
            } finally {
                action.finish();
            }
        }

        private LocalHistoryAction startLocalHistoryAction(final VcsFileRevision revision) {
            return LocalHistory.getInstance().startAction(createGetActionTitle(revision));
        }

        private String createGetActionTitle(final VcsFileRevision revision) {
            return getIOFile().getAbsolutePath() + ": Get Version " + revision.getRevisionNumber();
        }

        private File getIOFile() {
            return _filePath.getIOFile();
        }

        private void write(byte[] revision) throws IOException {
            if (_filePath.getVirtualFile() == null) {
                writeContentToIOFile(revision);
            } else {
                Document document = _filePath.getDocument();
                if (document == null) {
                    writeContentToFile(revision);
                } else {
                    writeContentToDocument(document, revision);
                }
            }
        }

        private void writeContentToIOFile(byte[] revisionContent) throws IOException {
            FileOutputStream outputStream = new FileOutputStream(getIOFile());
            try {
                outputStream.write(revisionContent);
            } finally {
                outputStream.close();
            }
        }

        private void writeContentToFile(final byte[] revision) throws IOException {
            _filePath.getVirtualFile().setBinaryContent(revision);
        }

        private void writeContentToDocument(final Document document, byte[] revisionContent) throws IOException {
            final String content = StringUtil.convertLineSeparators(new String(revisionContent, _filePath.getCharset().name()));

            CommandProcessor.getInstance().executeCommand(_project, new Runnable() {
                public void run() {
                    document.replaceString(0, document.getTextLength(), content);
                }
            }, "Get Version", null);
        }
    }

    private class RefreshAction extends AnAction {
        public RefreshAction() {
            super("Refresh", "Refresh file history", IconLoader.getIcon("/actions/sync.png"));
        }

        @Override
        public void update(AnActionEvent e) {
            e.getPresentation().setEnabled(!refreshInProgress);
        }

        @Override
        public void actionPerformed(AnActionEvent e) {
            refresh();
        }
    }

    private class EnableExtendedScanAction extends ToggleAction {

        public EnableExtendedScanAction() {
            super("Scan all current branches/tags");
        }

        @Override
        public void update(AnActionEvent e) {
            super.update(e);
            e.getPresentation().setEnabled(!refreshInProgress);
        }

        @Override
        public boolean isSelected(AnActionEvent e) {
            return extendedScan;
        }

        @Override
        public void setSelected(AnActionEvent e, boolean state) {
            SVNRevisionGraphConfiguration.getInstance(_project).setExtendedScanEnabled(state);
            if (state != extendedScan) {
                extendedScan = state;
                refresh();
            }
        }
    }

    private static class RevisionShape extends mxRectangleShape {
        @Override
        public Color getFillColor(mxGraphics2DCanvas canvas, mxCellState state) {
            return state.getView().getGraph().isCellSelected(state.getCell())
                    ? SELECTED_REVISION_COLOR : REVISION_COLOR;
        }

        @Override
        public void paintShape(mxGraphics2DCanvas canvas, mxCellState state) {
            Rectangle rect = state.getRectangle();
            Rectangle innerRect = new Rectangle(rect.x + 1, rect.y + 1, rect.width - 1, rect.height - 1);

            // Paints the background
            if (configureGraphics(canvas, state, true))
            {
                canvas.fillShape(innerRect, hasShadow(canvas, state));
            }

            // Paints the foreground
            if (configureGraphics(canvas, state, false))
            {
                canvas.getGraphics().drawRect(rect.x, rect.y, rect.width,
                        rect.height);
            }
        }
    }
}
